/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch licenses this file to you under
 * the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.elasticsearch.painless;

import org.elasticsearch.common.Nullable;
import org.hamcrest.Matcher;

import static java.util.Collections.singletonMap;

/**
 * Superclass for testing array-like objects (arrays and lists).
 */
public abstract class ArrayLikeObjectTestCase extends ScriptTestCase {
    /**
     * Build the string for declaring the variable holding the array-like-object to test. So {@code int[]} for arrays and {@code List} for
     * lists.
     */
    protected abstract String declType(String valueType);
    /**
     * Build the string for calling the constructor for the array-like-object to test. So {@code new int[5]} for arrays and
     * {@code [0, 0, 0, 0, 0]} or {@code [null, null, null, null, null]} for lists.
     */
    protected abstract String valueCtorCall(String valueType, int size);
    /**
     * Matcher for the message of the out of bounds exceptions thrown for too negative or too positive offsets.
     */
    protected abstract Matcher<String> outOfBoundsExceptionMessageMatcher(int index, int size);

    private void arrayLoadStoreTestCase(boolean declareAsDef, String valueType, Object val, @Nullable Number valPlusOne) {
        String declType = declareAsDef ? "def" : declType(valueType);
        String valueCtorCall = valueCtorCall(valueType, 5);
        String decl = declType + " x = " + valueCtorCall;
        assertEquals(5, exec(decl + "; return x.length", true));
        assertEquals(val, exec(decl + "; x[ 0] = params.val; return x[ 0];", singletonMap("val", val), true));
        assertEquals(val, exec(decl + "; x[ 0] = params.val; return x[-5];", singletonMap("val", val), true));
        assertEquals(val, exec(decl + "; x[-5] = params.val; return x[-5];", singletonMap("val", val), true));

        expectOutOfBounds( 6, decl + "; return x[ 6]", val);
        expectOutOfBounds(-1, decl + "; return x[-6]", val);
        expectOutOfBounds( 6, decl + "; x[ 6] = params.val; return 0", val);
        expectOutOfBounds(-1, decl + "; x[-6] = params.val; return 0", val);

        if (valPlusOne != null) {
            assertEquals(val,        exec(decl + "; x[0] = params.val; x[ 0] = x[ 0]++; return x[0];", singletonMap("val", val), true));
            assertEquals(val,        exec(decl + "; x[0] = params.val; x[ 0] = x[-5]++; return x[0];", singletonMap("val", val), true));
            assertEquals(valPlusOne, exec(decl + "; x[0] = params.val; x[ 0] = ++x[ 0]; return x[0];", singletonMap("val", val), true));
            assertEquals(valPlusOne, exec(decl + "; x[0] = params.val; x[ 0] = ++x[-5]; return x[0];", singletonMap("val", val), true));
            assertEquals(valPlusOne, exec(decl + "; x[0] = params.val; x[ 0]++        ; return x[0];", singletonMap("val", val), true));
            assertEquals(valPlusOne, exec(decl + "; x[0] = params.val; x[-5]++        ; return x[0];", singletonMap("val", val), true));
            assertEquals(valPlusOne, exec(decl + "; x[0] = params.val; x[ 0] += 1     ; return x[0];", singletonMap("val", val), true));
            assertEquals(valPlusOne, exec(decl + "; x[0] = params.val; x[-5] += 1     ; return x[0];", singletonMap("val", val), true));

            expectOutOfBounds( 6, decl + "; return x[ 6]++", val);
            expectOutOfBounds(-1, decl + "; return x[-6]++", val);
            expectOutOfBounds( 6, decl + "; return ++x[ 6]", val);
            expectOutOfBounds(-1, decl + "; return ++x[-6]", val);
            expectOutOfBounds( 6, decl + "; x[ 6] += 1; return 0", val);
            expectOutOfBounds(-1, decl + "; x[-6] += 1; return 0", val);
        }
    }

    private void expectOutOfBounds(int index, String script, Object val) {
        IndexOutOfBoundsException e = expectScriptThrows(IndexOutOfBoundsException.class, () ->
            exec(script, singletonMap("val", val), true));
        try {
            /* If this fails you *might* be missing -XX:-OmitStackTraceInFastThrow in the test jvm
             * In Eclipse you can add this by default by going to Preference->Java->Installed JREs,
             * clicking on the default JRE, clicking edit, and adding the flag to the
             * "Default VM Arguments".
             */
            assertThat(e.getMessage(), outOfBoundsExceptionMessageMatcher(index, 5));
        } catch (AssertionError ae) {
            ae.addSuppressed(e);   // Mark the exception we are testing as suppressed so we get its stack trace.
            throw ae;
        }
    }

    public void testInts() {         arrayLoadStoreTestCase(false, "int",    5,         6); }
    public void testIntsInDef() {    arrayLoadStoreTestCase(true,  "int",    5,         6); }
    public void testLongs() {        arrayLoadStoreTestCase(false, "long",   5L,        6L); }
    public void testLongsInDef() {   arrayLoadStoreTestCase(true,  "long",   5L,        6L); }
    public void testShorts() {       arrayLoadStoreTestCase(false, "short",  (short) 5, (short) 6); }
    public void testShortsInDef() {  arrayLoadStoreTestCase(true,  "short",  (short) 5, (short) 6); }
    public void testBytes() {        arrayLoadStoreTestCase(false, "byte",   (byte) 5,  (byte) 6); }
    public void testBytesInDef() {   arrayLoadStoreTestCase(true,  "byte",   (byte) 5,  (byte) 6); }
    public void testFloats() {       arrayLoadStoreTestCase(false, "float",  5.0f,      6.0f); }
    public void testFloatsInDef() {  arrayLoadStoreTestCase(true,  "float",  5.0f,      6.0f); }
    public void testDoubles() {      arrayLoadStoreTestCase(false, "double", 5.0d,      6.0d); }
    public void testDoublesInDef() { arrayLoadStoreTestCase(true,  "double", 5.0d,      6.0d); }
    public void testStrings() {      arrayLoadStoreTestCase(false, "String", "cat",     null); }
    public void testStringsInDef() { arrayLoadStoreTestCase(true,  "String", "cat",     null); }
    public void testDef() {          arrayLoadStoreTestCase(true,  "def",    5,         null); }
}
